Skip to content

feat(user-journey): capture page-view referrer for organic/referral attribution#69

Merged
AminHakem merged 2 commits into
mainfrom
capture-page-view-referrer
Jul 21, 2026
Merged

feat(user-journey): capture page-view referrer for organic/referral attribution#69
AminHakem merged 2 commits into
mainfrom
capture-page-view-referrer

Conversation

@AminHakem

Copy link
Copy Markdown
Collaborator

Summary

  • Adds document.referrer to every page_view event emitted by Surface Tag (both initial page load and SPA route changes), surfaced as data.payload.referrer.
  • Centralises page-view creation in a shared createPageViewEvent() helper so initial-load and SPA events use the same schema consistently.
  • Adds SSR/non-browser safety guards for document, window, navigator, and localStorage in user-journey.ts and cookies.ts.
  • Adds focused unit tests in test/unit/user-journey.test.js covering: Google referrer, direct/empty referrer, no-document safety, sendBeacon transport, and SPA navigation.

Backend follow-up needed: confirm the Forms lead/session ingestion pipeline persists and reads data.payload.referrer for page_view events.

🤖 Generated with Claude Code

Captures document.referrer at page-load time and sends it as
data.payload.referrer in every page_view event — both initial load
and SPA route changes — so organic/referral attribution is recorded
even when a visitor never interacts with a form.

Centralises page-view creation in createPageViewEvent(), adds SSR
guards for document/window/navigator/localStorage, and regenerates
the bundle. Adds focused unit tests (test/unit/) and a test:unit
npm script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds document.referrer to every page_view event, centralises event construction in a new createPageViewEvent helper shared by both initial-load and SPA tracking paths, and backfills SSR/non-browser safety guards across user-journey.ts and cookies.ts.

  • Referrer capture: getBrowserReferrer() safely reads document.referrer with an SSR guard; the value is included in data.payload.referrer for all page_view events going forward. The in-code comment correctly documents that document.referrer is frozen to the original hard-navigation source on SPA routes.
  • SSR guards: initializeUserJourneyTracking and updateUserJourneyOnRouteChange now return early when window is undefined; parseCookies, setCookie, and deleteCookie return early when document is undefined; trackToRedis guards navigator before checking sendBeacon. clearUserJourney (not modified here) and the underlying getJourneyCookieDomain in journey-cookies.ts remain unguarded — a narrow gap if that cleanup path is ever reached server-side.
  • Tests: Five unit tests cover the happy path, empty referrer, no-document safety, sendBeacon serialization, and SPA route-change events using Node.js's built-in test runner.

Confidence Score: 5/5

Safe to merge. The new referrer field is additive and all changed code paths have correct SSR guards.

The core tracking and cookie code paths are handled correctly. The only gap — clearUserJourney calling the unguarded getJourneyCookieDomain — was pre-existing and is outside the scope of the changes made here. All five new tests exercise the intended behaviour.

src/store/journey-cookies.tsgetJourneyCookieDomain accesses window without a typeof guard, which could surface if clearUserJourney is ever called in a non-browser environment.

Reviews (2): Last reviewed commit: "test(user-journey): harden beacon covera..." | Re-trigger Greptile

Comment thread test/unit/user-journey.test.js
Comment on lines +5 to +21
let userJourney;

async function loadUserJourney() {
if (userJourney) return userJourney;

const result = await esbuild.build({
entryPoints: ["src/store/user-journey.ts"],
bundle: true,
format: "cjs",
platform: "browser",
target: "es2020",
write: false,
});
const module = { exports: {} };
new Function("module", "exports", result.outputFiles[0].text)(module, module.exports);
userJourney = module.exports;
return userJourney;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Shared module cache hides inter-test state leakage

loadUserJourney() evaluates the esbuild bundle exactly once and caches the result in the module-level userJourney variable. The bundled output includes identify.ts, which holds module-level mutable variables environmentId and identifyInProgress. If a future test calls identifyLead and leaves identifyInProgress = true (e.g., due to an error mid-flight), every subsequent test that calls getLeadDataWithTTL via getSurfaceLeadDataSafely will hit the waitForCachedData path and time out. Clearing shared mutable state between tests (or rebuilding the module per test) would prevent this latent coupling.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change here: the current page-view tests never call identifyLead, so they do not mutate identifyInProgress or environmentId. identifyLead also resets identifyInProgress in a finally block on both success and failure. The cached exports avoid rebundling for every test without introducing state leakage in this suite; future identifyLead-specific tests should own any additional isolation their lifecycle requires.

@AminHakem
AminHakem merged commit a0aa494 into main Jul 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants